use std::{
    collections::BTreeMap,
    sync::{
        Arc,
        LazyLock,
    },
    time::Duration,
};

use common::{
    log_lines::LogLevel,
    runtime::{
        JoinSet,
        Runtime,
        UnixTimestamp,
    },
    types::EnvVarValue,
    value::NamespacedTableMapping,
};
use deno_core::{
    sourcemap::SourceMap,
    v8,
};
use futures::{
    future,
    FutureExt,
};
use isolate::{
    environment::{
        crypto_rng::CryptoRng,
        AsyncOpRequest,
        IsolateEnvironment,
        ModuleCodeCacheResult,
    },
    ConcurrencyPermit,
    Timeout,
};
use model::modules::module_versions::FullModuleSource;
use rand::{
    Rng,
    SeedableRng,
};
use rand_chacha::ChaCha12Rng;
use runtime::testing::TestRuntime;
use serde_json::Value as JsonValue;

// NB: These files are generated by the *isolate* crate's build script.
pub const TEST_SOURCE: &str = include_str!("../../../../../npm-packages/simulation/dist/main.js");
pub const TEST_SOURCE_MAP_STR: &str =
    include_str!("../../../../../npm-packages/simulation/dist/main.js.map");
pub static TEST_SOURCE_MAP: LazyLock<SourceMap> = LazyLock::new(|| {
    SourceMap::from_slice(TEST_SOURCE_MAP_STR.as_bytes()).expect("Invalid source map")
});

pub struct TestEnvironment {
    rt: TestRuntime,
    rng: ChaCha12Rng,

    next_timer_id: usize,
    timers: JoinSet<usize>,
    timer_resolvers: BTreeMap<usize, v8::Global<v8::PromiseResolver>>,
}

impl TestEnvironment {
    pub fn new(rt: TestRuntime) -> Self {
        let rng = ChaCha12Rng::from_seed(rt.rng().random());
        Self {
            rt,
            rng,

            next_timer_id: 0,
            timers: JoinSet::new(),
            timer_resolvers: BTreeMap::new(),
        }
    }
}

impl IsolateEnvironment<TestRuntime> for TestEnvironment {
    async fn lookup_source(
        &mut self,
        path: &str,
        _timeout: &mut Timeout<TestRuntime>,
        _permit: &mut Option<ConcurrencyPermit>,
    ) -> anyhow::Result<Option<(Arc<FullModuleSource>, ModuleCodeCacheResult)>> {
        if path != "test.js" {
            return Ok(None);
        }
        Ok(Some((
            Arc::new(FullModuleSource {
                source: TEST_SOURCE.into(),
                source_map: Some(TEST_SOURCE_MAP_STR.to_string()),
            }),
            ModuleCodeCacheResult::noop(),
        )))
    }

    fn syscall(&mut self, _name: &str, _args: JsonValue) -> anyhow::Result<JsonValue> {
        panic!("syscall() unimplemented");
    }

    fn start_async_syscall(
        &mut self,
        name: String,
        args: JsonValue,
        _resolver: v8::Global<v8::PromiseResolver>,
    ) -> anyhow::Result<()> {
        tracing::info!("Ignoring async syscall: {name:?} {args:?}");
        Ok(())
    }

    fn trace(&mut self, level: LogLevel, messages: Vec<String>) -> anyhow::Result<()> {
        for message in messages {
            match level {
                LogLevel::Debug => tracing::debug!("[console] {message}"),
                LogLevel::Error => tracing::error!("[console] {message}"),
                LogLevel::Warn => tracing::warn!("[console] {message}"),
                LogLevel::Info => tracing::info!("[console] {message}"),
                LogLevel::Log => tracing::info!("[console] {message}"),
            }
        }
        Ok(())
    }

    fn rng(&mut self) -> anyhow::Result<&mut ChaCha12Rng> {
        Ok(&mut self.rng)
    }

    fn crypto_rng(&mut self) -> anyhow::Result<CryptoRng> {
        anyhow::bail!("CryptoRng not allowed in simulation")
    }

    fn unix_timestamp(&mut self) -> anyhow::Result<UnixTimestamp> {
        Ok(self.rt.unix_timestamp())
    }

    fn get_environment_variable(
        &mut self,
        _name: common::types::EnvVarName,
    ) -> anyhow::Result<Option<EnvVarValue>> {
        Ok(None)
    }

    fn get_all_table_mappings(&mut self) -> anyhow::Result<NamespacedTableMapping> {
        panic!("get_all_table_mappings() unimplemented");
    }

    fn start_async_op(
        &mut self,
        request: AsyncOpRequest,
        resolver: v8::Global<v8::PromiseResolver>,
    ) -> anyhow::Result<()> {
        match request {
            AsyncOpRequest::Sleep { until, .. } => {
                let id = self.next_timer_id;
                self.next_timer_id += 1;

                let now = self.rt.unix_timestamp();
                let duration = if until > now {
                    until - now
                } else {
                    Duration::ZERO
                };
                self.timers
                    .spawn("timer", tokio::time::sleep(duration).map(move |_| id));
                self.timer_resolvers.insert(id, resolver);
            },
            req => {
                tracing::debug!("Ignoring async op request: {req:?}");
            },
        }
        Ok(())
    }

    fn user_timeout(&self) -> Duration {
        Duration::from_secs(60 * 60 * 24)
    }

    fn system_timeout(&self) -> Duration {
        Duration::from_secs(60 * 60 * 24)
    }
}

impl TestEnvironment {
    pub async fn next_timer(&mut self) -> anyhow::Result<v8::Global<v8::PromiseResolver>> {
        let Some(timer) = self.timers.join_next().await else {
            return future::pending().await;
        };
        let timer_id = timer?;
        let resolver = self
            .timer_resolvers
            .remove(&timer_id)
            .ok_or_else(|| anyhow::anyhow!("Timer resolver not found"))?;
        Ok(resolver)
    }
}
